Анализ окупаемости рекламы приложения Procrastinate Pro+¶

Содержание

  • 1  Загрузка данных и подготовка к анализу
  • 2  Предобработка данных
    • 2.1  Изменение типов данных
    • 2.2  Обработка пропусков
    • 2.3  Обработка дубликатов
  • 3  Функции для расчёта и анализа LTV, ROI, удержания и конверсии
  • 4  Исследовательский анализ данных
    • 4.1  Профили пользователей
    • 4.2  Географическое распределение пользователей
    • 4.3  Распределение пользователей по устройствам
    • 4.4  Распределение пользователей по каналам привлечения
  • 5  Маркетинг
    • 5.1  Общая сумма расходов на маркетинг
    • 5.2  Распределение рекламных расходов по источникам
    • 5.3  Динамика изменения рекламных расходов по источникам
    • 5.4  Средняя стоимость привлечения по источнику
  • 6  Окупаемость рекламы
    • 6.1  Окупаемость рекламы в целом
    • 6.2  Конверсия пользователей
    • 6.3  Удержание пользователей
    • 6.4  Окупаемость рекламы с разбивкой по устройствам
    • 6.5  Окупаемость рекламы с разбивкой по странам
    • 6.6  Окупаемость рекламы с разбивкой по рекламным каналам
  • 7  Выводы

Краткое описание проекта: Несмотря на огромные вложения в рекламу, последние несколько месяцев развлекательное приложение Procrastinate Pro+ терпит убытки. Задача исследования в том, чтобы разобраться в причинах и помочь компании выйти в плюс.

Описание данных:

Структура visits_info_short.csv:

  • User Id — уникальный идентификатор пользователя,
  • Region — страна пользователя,
  • Device — тип устройства пользователя,
  • Channel — идентификатор источника перехода,
  • Session Start — дата и время начала сессии,
  • Session End — дата и время окончания сессии.

Структура orders_info_short.csv:

  • User Id — уникальный идентификатор пользователя,
  • Event Dt — дата и время покупки,
  • Revenue — сумма заказа.

Структура costs_info_short.csv:

  • dt — дата проведения рекламной кампании,
  • Channel — идентификатор рекламного источника,
  • costs — расходы на эту кампанию.

План работы:

  1. Выгрузка данных и первичное знакомство с данными.
  2. Изменение типов данных, обработка дубликатов и пропусков.
  3. Внесение используемых при анализе функций.
  4. Выгрузка профилей пользователей.
  5. Анализ распределения пользователей по странам, устройствам и каналам привлечения.
  6. Расчет расходов на маркетинг по источникам привлечения.
  7. Анализ окупаемости рекламы, конверсии и удержания пользователей.
  8. Выводы.

Цель исследования: анализ окупаемости рекламы и поиск новых точек роста.

In [1]:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns 
from scipy import stats as st
from scipy.stats import binom, norm
from math import sqrt
from IPython.display import display
from datetime import datetime, timedelta

Загрузка данных и подготовка к анализу¶

In [2]:
visits = pd.read_csv('visits_info_short.csv')
orders = pd.read_csv('orders_info_short.csv')
costs = pd.read_csv('costs_info_short.csv')
In [3]:
visits.info()
orders.info()
costs.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 309901 entries, 0 to 309900
Data columns (total 6 columns):
 #   Column         Non-Null Count   Dtype 
---  ------         --------------   ----- 
 0   User Id        309901 non-null  int64 
 1   Region         309901 non-null  object
 2   Device         309901 non-null  object
 3   Channel        309901 non-null  object
 4   Session Start  309901 non-null  object
 5   Session End    309901 non-null  object
dtypes: int64(1), object(5)
memory usage: 14.2+ MB
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 40212 entries, 0 to 40211
Data columns (total 3 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   User Id   40212 non-null  int64  
 1   Event Dt  40212 non-null  object 
 2   Revenue   40212 non-null  float64
dtypes: float64(1), int64(1), object(1)
memory usage: 942.6+ KB
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1800 entries, 0 to 1799
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   dt       1800 non-null   object 
 1   Channel  1800 non-null   object 
 2   costs    1800 non-null   float64
dtypes: float64(1), object(2)
memory usage: 42.3+ KB

Предобработка данных¶

Изменение типов данных¶

Из первичного знакомства с данными видно, что у столбцов со временем строковый формат, поэтому его нужно поменять на datetime. Также нужно поменять названия столбцов: применить нижний регистр и заменить пробелы нижним подчеркиванием.

In [4]:
visits.columns = visits.columns.str.lower()
orders.columns = orders.columns.str.lower()
costs.columns = costs.columns.str.lower()
In [5]:
visits.columns = visits.columns.str.replace(' ', '_')
orders.columns = orders.columns.str.replace(' ', '_')
costs.columns = costs.columns.str.replace(' ', '_')
In [6]:
visits['session_start'] = pd.to_datetime(visits['session_start'])
visits['session_end'] = pd.to_datetime(visits['session_end'])
orders['event_dt'] = pd.to_datetime(orders['event_dt'])
costs['dt'] = pd.to_datetime(costs['dt']).dt.date

Обработка пропусков¶

Из первичного знакомства с данными видно, что пропуски отсутствуют.

Обработка дубликатов¶

In [7]:
visits.duplicated().sum()
Out[7]:
0
In [8]:
orders.duplicated().sum()
Out[8]:
0
In [9]:
costs.duplicated().sum()
Out[9]:
0

Явные дубликаты отсутствуют. Из самих данных сложно предоположить наличие неявных дубликатов, можно лишь проверить, что каждый пользователь не относится к двум странам сразу, что было бы странно:

In [10]:
visits.query('user_id.duplicated()==True and region.duplicated()==False')['user_id'].nunique()
Out[10]:
0

Предварительная обработка данных показала, что данные очень высокого качества: в них полностью отсутствуют пропуски и дубликаты. Единственное, что пришлось исправить, это поменять тип данных в столбцах с датой и временем.

Функции для расчёта и анализа LTV, ROI, удержания и конверсии¶

In [11]:
def get_profiles(sessions, orders, ad_costs):

    # находим параметры первых посещений
    profiles = (
        sessions.sort_values(by=['user_id', 'session_start'])
        .groupby('user_id')
        .agg(
            {
                'session_start': 'first',
                'channel': 'first',
                'device': 'first',
                'region': 'first',
            }
        )
        .rename(columns={'session_start': 'first_ts'})
        .reset_index()
    )

    # для когортного анализа определяем дату первого посещения
    # и первый день месяца, в который это посещение произошло
    profiles['dt'] = profiles['first_ts'].dt.date
    profiles['month'] = profiles['first_ts'].astype('datetime64[M]')

    # добавляем признак платящих пользователей
    profiles['payer'] = profiles['user_id'].isin(orders['user_id'].unique())

    # считаем количество уникальных пользователей
    # с одинаковыми источником и датой привлечения
    new_users = (
        profiles.groupby(['dt', 'channel'])
        .agg({'user_id': 'nunique'})
        .rename(columns={'user_id': 'unique_users'})
        .reset_index()
    )

    # объединяем траты на рекламу и число привлечённых пользователей
    ad_costs = ad_costs.merge(new_users, on=['dt', 'channel'], how='left')

    # делим рекламные расходы на число привлечённых пользователей
    ad_costs['acquisition_cost'] = ad_costs['costs'] / ad_costs['unique_users']

    # добавляем стоимость привлечения в профили
    profiles = profiles.merge(
        ad_costs[['dt', 'channel', 'acquisition_cost']],
        on=['dt', 'channel'],
        how='left',
    )

    # стоимость привлечения органических пользователей равна нулю
    profiles['acquisition_cost'] = profiles['acquisition_cost'].fillna(0)

    return profiles
In [12]:
def get_retention(
    profiles,
    sessions,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):
    
     # исключаем органиков
    profiles = profiles.query('channel!="organic"')
    
    # добавляем столбец payer в передаваемый dimensions список
    dimensions = ['payer'] + dimensions

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')

    # собираем «сырые» данные для расчёта удержания
    result_raw = result_raw.merge(
        sessions[['user_id', 'session_start']], on='user_id', how='left'
    )
    result_raw['lifetime'] = (
        result_raw['session_start'] - result_raw['first_ts']
    ).dt.days

    # функция для группировки таблицы по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        result = df.pivot_table(
            index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
        )
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        result = result.div(result['cohort_size'], axis=0)
        result = result[['cohort_size'] + list(range(horizon_days))]
        result['cohort_size'] = cohort_sizes
        return result

    # получаем таблицу удержания
    result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)

    # получаем таблицу динамики удержания
    result_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    # возвращаем обе таблицы и сырые данные
    return result_raw, result_grouped, result_in_time
In [13]:
def get_conversion(
    profiles,
    purchases,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):
    
    # исключаем органиков
    profiles = profiles.query('channel!="organic"')
    
    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')

    # определяем дату и время первой покупки для каждого пользователя
    first_purchases = (
        purchases.sort_values(by=['user_id', 'event_dt'])
        .groupby('user_id')
        .agg({'event_dt': 'first'})
        .reset_index()
    )

    # добавляем данные о покупках в профили
    result_raw = result_raw.merge(
        first_purchases[['user_id', 'event_dt']], on='user_id', how='left'
    )

    # рассчитываем лайфтайм для каждой покупки
    result_raw['lifetime'] = (
        result_raw['event_dt'] - result_raw['first_ts']
    ).dt.days

    # группируем по cohort, если в dimensions ничего нет
    if len(dimensions) == 0:
        result_raw['cohort'] = 'All users' 
        dimensions = dimensions + ['cohort']

    # функция для группировки таблицы по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        result = df.pivot_table(
            index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
        )
        result = result.fillna(0).cumsum(axis = 1)
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        # делим каждую «ячейку» в строке на размер когорты
        # и получаем conversion rate
        result = result.div(result['cohort_size'], axis=0)
        result = result[['cohort_size'] + list(range(horizon_days))]
        result['cohort_size'] = cohort_sizes
        return result

    # получаем таблицу конверсии
    result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)

    # для таблицы динамики конверсии убираем 'cohort' из dimensions
    if 'cohort' in dimensions: 
        dimensions = []

    # получаем таблицу динамики конверсии
    result_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    # возвращаем обе таблицы и сырые данные
    return result_raw, result_grouped, result_in_time 
In [14]:
def get_ltv(
    profiles,
    purchases,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):
    
    # исключаем органиков
    profiles = profiles.query('channel!="organic"')
    
    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
    # добавляем данные о покупках в профили
    result_raw = result_raw.merge(
        purchases[['user_id', 'event_dt', 'revenue']], on='user_id', how='left'
    )
    # рассчитываем лайфтайм пользователя для каждой покупки
    result_raw['lifetime'] = (
        result_raw['event_dt'] - result_raw['first_ts']
    ).dt.days
    # группируем по cohort, если в dimensions ничего нет
    if len(dimensions) == 0:
        result_raw['cohort'] = 'All users'
        dimensions = dimensions + ['cohort']

    # функция группировки по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        # строим «треугольную» таблицу выручки
        result = df.pivot_table(
            index=dims, columns='lifetime', values='revenue', aggfunc='sum'
        )
        # находим сумму выручки с накоплением
        result = result.fillna(0).cumsum(axis=1)
        # вычисляем размеры когорт
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        # объединяем размеры когорт и таблицу выручки
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        # считаем LTV: делим каждую «ячейку» в строке на размер когорты
        result = result.div(result['cohort_size'], axis=0)
        # исключаем все лайфтаймы, превышающие горизонт анализа
        result = result[['cohort_size'] + list(range(horizon_days))]
        # восстанавливаем размеры когорт
        result['cohort_size'] = cohort_sizes

        # собираем датафрейм с данными пользователей и значениями CAC, 
        # добавляя параметры из dimensions
        cac = df[['user_id', 'acquisition_cost'] + dims].drop_duplicates()

        # считаем средний CAC по параметрам из dimensions
        cac = (
            cac.groupby(dims)
            .agg({'acquisition_cost': 'mean'})
            .rename(columns={'acquisition_cost': 'cac'})
        )

        # считаем ROI: делим LTV на CAC
        roi = result.div(cac['cac'], axis=0)

        # удаляем строки с бесконечным ROI
        roi = roi[~roi['cohort_size'].isin([np.inf])]

        # восстанавливаем размеры когорт в таблице ROI
        roi['cohort_size'] = cohort_sizes

        # добавляем CAC в таблицу ROI
        roi['cac'] = cac['cac']

        # в финальной таблице оставляем размеры когорт, CAC
        # и ROI в лайфтаймы, не превышающие горизонт анализа
        roi = roi[['cohort_size', 'cac'] + list(range(horizon_days))]

        # возвращаем таблицы LTV и ROI
        return result, roi

    # получаем таблицы LTV и ROI
    result_grouped, roi_grouped = group_by_dimensions(
        result_raw, dimensions, horizon_days
    )

    # для таблиц динамики убираем 'cohort' из dimensions
    if 'cohort' in dimensions:
        dimensions = []

    # получаем таблицы динамики LTV и ROI
    result_in_time, roi_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    return (
        result_raw,  # сырые данные
        result_grouped,  # таблица LTV
        result_in_time,  # таблица динамики LTV
        roi_grouped,  # таблица ROI
        roi_in_time,  # таблица динамики ROI
    ) 
In [15]:
def filter_data(df, window):
    # для каждого столбца применяем скользящее среднее
    for column in df.columns.values:
        df[column] = df[column].rolling(window).mean() 
    return df 
In [16]:
def plot_retention(retention, retention_history, horizon, window=7):

    # задаём размер сетки для графиков
    plt.figure(figsize=(15, 10))

    # исключаем размеры когорт и удержание первого дня
    retention = retention.drop(columns=['cohort_size', 0])
    # в таблице динамики оставляем только нужный лайфтайм
    retention_history = retention_history.drop(columns=['cohort_size'])[
        [horizon - 1]
    ]

    # если в индексах таблицы удержания только payer,
    # добавляем второй признак — cohort
    if retention.index.nlevels == 1:
        retention['cohort'] = 'All users'
        retention = retention.reset_index().set_index(['cohort', 'payer'])

    # в таблице графиков — два столбца и две строки, четыре ячейки
    # в первой строим кривые удержания платящих пользователей
    ax1 = plt.subplot(2, 2, 1)
    retention.query('payer == True').droplevel('payer').T.plot(
        grid=True, ax=ax1, cmap='tab20'
    )
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('Удержание платящих пользователей')

    # во второй ячейке строим кривые удержания неплатящих
    # вертикальная ось — от графика из первой ячейки
    ax2 = plt.subplot(2, 2, 2, sharey=ax1)
    retention.query('payer == False').droplevel('payer').T.plot(
        grid=True, ax=ax2, cmap='tab20'
    )
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('Удержание неплатящих пользователей')

    # в третьей ячейке — динамика удержания платящих
    ax3 = plt.subplot(2, 2, 3)
    # получаем названия столбцов для сводной таблицы
    columns = [
        name
        for name in retention_history.index.names
        if name not in ['dt', 'payer']
    ]
    # фильтруем данные и строим график
    filtered_data = retention_history.query('payer == True').pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax3, cmap='tab20')
    plt.xlabel('Дата привлечения')
    plt.title(
        'Динамика удержания платящих пользователей на {}-й день'.format(
            horizon
        )
    )

    # в чётвертой ячейке — динамика удержания неплатящих
    ax4 = plt.subplot(2, 2, 4, sharey=ax3)
    # фильтруем данные и строим график
    filtered_data = retention_history.query('payer == False').pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax4, cmap='tab20')
    plt.xlabel('Дата привлечения')
    plt.title(
        'Динамика удержания неплатящих пользователей на {}-й день'.format(
            horizon
        )
    )
    
    plt.tight_layout()
    plt.show() 
In [17]:
def plot_conversion(conversion, conversion_history, horizon, window=7):

    # задаём размер сетки для графиков
    plt.figure(figsize=(15, 5))

    # исключаем размеры когорт
    conversion = conversion.drop(columns=['cohort_size'])
    # в таблице динамики оставляем только нужный лайфтайм
    conversion_history = conversion_history.drop(columns=['cohort_size'])[
        [horizon - 1]
    ]

    # первый график — кривые конверсии
    ax1 = plt.subplot(1, 2, 1)
    conversion.T.plot(grid=True, ax=ax1, cmap='tab20')
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('Конверсия пользователей')

    # второй график — динамика конверсии
    ax2 = plt.subplot(1, 2, 2, sharey=ax1)
    columns = [
        # столбцами сводной таблицы станут все столбцы индекса, кроме даты
        name for name in conversion_history.index.names if name not in ['dt']
    ]
    filtered_data = conversion_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax2, cmap='tab20')
    plt.xlabel('Дата привлечения')
    plt.title('Динамика конверсии пользователей на {}-й день'.format(horizon))

    plt.tight_layout()
    plt.show()
In [18]:
def plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon, window=7):

    # задаём сетку отрисовки графиков
    plt.figure(figsize=(20, 10))

    # из таблицы ltv исключаем размеры когорт
    ltv = ltv.drop(columns=['cohort_size'])
    # в таблице динамики ltv оставляем только нужный лайфтайм
    ltv_history = ltv_history.drop(columns=['cohort_size'])[[horizon - 1]]

    # стоимость привлечения запишем в отдельный фрейм
    cac_history = roi_history[['cac']]

    # из таблицы roi исключаем размеры когорт и cac
    roi = roi.drop(columns=['cohort_size', 'cac'])
    # в таблице динамики roi оставляем только нужный лайфтайм
    roi_history = roi_history.drop(columns=['cohort_size', 'cac'])[
        [horizon - 1]
    ]

    # первый график — кривые ltv
    ax1 = plt.subplot(2, 3, 1)
    ltv.T.plot(grid=True, ax=ax1, cmap='tab20')
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('LTV')

    # второй график — динамика ltv
    ax2 = plt.subplot(2, 3, 2, sharey=ax1)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in ltv_history.index.names if name not in ['dt']]
    filtered_data = ltv_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax2, cmap='tab20')
    plt.xlabel('Дата привлечения')
    plt.title('Динамика LTV пользователей на {}-й день'.format(horizon))

    # третий график — динамика cac
    ax3 = plt.subplot(2, 3, 3, sharey=ax1)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in cac_history.index.names if name not in ['dt']]
    filtered_data = cac_history.pivot_table(
        index='dt', columns=columns, values='cac', aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax3, cmap='tab20')
    plt.xlabel('Дата привлечения')
    plt.title('Динамика стоимости привлечения пользователей')

    # четвёртый график — кривые roi
    ax4 = plt.subplot(2, 3, 4)
    roi.T.plot(grid=True, ax=ax4, cmap='tab20')
    plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('ROI')

    # пятый график — динамика roi
    ax5 = plt.subplot(2, 3, 5, sharey=ax4)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in roi_history.index.names if name not in ['dt']]
    filtered_data = roi_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax5, cmap='tab20')
    plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
    plt.xlabel('Дата привлечения')
    plt.title('Динамика ROI пользователей на {}-й день'.format(horizon))

    plt.tight_layout()
    plt.show() 

Исследовательский анализ данных¶

Профили пользователей¶

In [19]:
profiles = get_profiles(visits, orders, costs)
In [20]:
profiles
Out[20]:
user_id first_ts channel device region dt month payer acquisition_cost
0 599326 2019-05-07 20:58:57 FaceBoom Mac United States 2019-05-07 2019-05-01 True 1.088172
1 4919697 2019-07-09 12:46:07 FaceBoom iPhone United States 2019-07-09 2019-07-01 False 1.107237
2 6085896 2019-10-01 09:58:33 organic iPhone France 2019-10-01 2019-10-01 False 0.000000
3 22593348 2019-08-22 21:35:48 AdNonSense PC Germany 2019-08-22 2019-08-01 False 0.988235
4 31989216 2019-10-02 00:07:44 YRabbit iPhone United States 2019-10-02 2019-10-01 False 0.230769
... ... ... ... ... ... ... ... ... ...
150003 999956196527 2019-09-28 08:33:02 TipTop iPhone United States 2019-09-28 2019-09-01 False 3.500000
150004 999975439887 2019-10-21 00:35:17 organic PC UK 2019-10-21 2019-10-01 False 0.000000
150005 999976332130 2019-07-23 02:57:06 TipTop iPhone United States 2019-07-23 2019-07-01 False 2.600000
150006 999979924135 2019-09-28 21:28:09 MediaTornado PC United States 2019-09-28 2019-09-01 False 0.205714
150007 999999563947 2019-10-18 19:57:25 organic iPhone United States 2019-10-18 2019-10-01 False 0.000000

150008 rows × 9 columns

In [21]:
min_date = profiles['dt'].min() #минимальная дата привлечения 
min_date
Out[21]:
datetime.date(2019, 5, 1)
In [22]:
max_date = profiles['dt'].max()
max_date
Out[22]:
datetime.date(2019, 10, 27)

Получили профили пользователей, проверили, что минимальная дата привлечения 01.05.2019, а максимальная - 27.10.2019, что совпадает с периодом, за которой были собраны данные.

Географическое распределение пользователей¶

Напишем функцию для формирования и вывода на экран сводных таблиц:

In [23]:
def pivot_print(index_name):
    table = profiles.pivot_table(index=index_name, values=['user_id', 'payer'],
                         aggfunc={'user_id':'nunique', 'payer':['mean', 'sum']}).droplevel(1, axis=1)
    table['user_ratio'] = (table['user_id']/table['user_id'].sum())
    table.columns = ['payer_ratio', 'payer', 'user_id', 'user_ratio']
    table['payer_ratio'] = (table['payer_ratio'])
    return table 
In [24]:
def pivot_per(table):
    table = table.style.format({'payer_ratio': '{:,.2%}'.format, 'user_ratio': '{:,.2%}'.format})
    return table 
In [25]:
region_data = pivot_print('region') 
pivot_per(region_data)
Out[25]:
payer_ratio payer user_id user_ratio
region
France 3.80% 663 17450 11.63%
Germany 4.11% 616 14981 9.99%
UK 3.98% 700 17575 11.72%
United States 6.90% 6902 100002 66.66%
In [26]:
region_data.plot.pie(autopct='%.2f%%', y='user_id', ylabel='', legend=False, title='Распределение пользователей по странам',
                    ax=plt.subplot(1,2,1), figsize=(12,15))
region_data.plot.pie(autopct='%.2f%%', y='payer', ylabel='', legend=False, 
                     title='Распределение платящих пользователей по странам', ax=plt.subplot(1,2,2), figsize=(12,15))
plt.tight_layout()
plt.show()
No description has been provided for this image
In [27]:
region_payer = (region_data['payer_ratio']*100).sort_values().plot.barh(title='Рейтинг стран по доле платящих пользователей')
region_payer.set(xlabel='Доля платящих пользователей, %', ylabel='Страна');
No description has been provided for this image

Пользователи приходят из Франции, Германии, Великобритании, США. Примерно 2/3 пользователей из США, примерно одинаковое количество пользователей из Великобритании и Франции (11,72% и 11,63% соответственно), оставшиеся 9,99% из Германии.
По количеству платящих пользователей абсолютным лидером также является США (на эту страну приходится примерно 77,72% всех платящих пользователей), остальные страны имеют примерно одинаковое количество платящих пользователей.
По доле платящих абсолютным лидером также является США (6,9%), далее с примерно одинаковыми результатами следуют Германия (4,1%), Великобритания (4%), Франция (3,8%).
Стоит также отметить низкую долю платящих пользователей в целом.

Распределение пользователей по устройствам¶

In [28]:
device_data = pivot_print('device')
pivot_per(device_data)
Out[28]:
payer_ratio payer user_id user_ratio
device
Android 5.85% 2050 35032 23.35%
Mac 6.36% 1912 30042 20.03%
PC 5.05% 1537 30455 20.30%
iPhone 6.21% 3382 54479 36.32%
In [29]:
device_data.plot.pie(autopct='%.2f%%', y='user_id', ylabel='', legend=False, 
                     title='Распределение пользователей по устройствам', ax=plt.subplot(1,2,1), figsize=(12,20))
device_data.plot.pie(autopct='%.2f%%', y='payer', ylabel='', legend=False, 
                     title='Распределение платящих пользователей по устройствам', ax=plt.subplot(1,2,2), figsize=(12,20))
plt.tight_layout()
plt.show()
No description has been provided for this image
In [30]:
device_payer = (device_data['payer_ratio']*100).sort_values().plot.barh(title='Рейтинг устройств по доле платящих пользователей')
device_payer.set(xlabel='Доля платящих пользователей, %', ylabel='Устройство');
No description has been provided for this image

Пользователи заходят в приложение через Mac, iPhone, Android, PC. Большинство пользователей используют iPhone (36,32%), далее следуют Android (23,35%), PC(20,3%), Mac (20,03%).
По количеству платящих лидером является iPhone (38,08%), далее следуют Android(23,08%), Mac(21,53%), PC(17,31%).
По доле платящих ситуация примерно одинаковая, показатель меняется от 5% на PC до 6,4% на Mac. На iPhone и Android доля платящих составляет примерно 6,2% и 5,9% соответственно.

Распределение пользователей по каналам привлечения¶

In [31]:
channel_data = pivot_print('channel')
pivot_per(channel_data)
Out[31]:
payer_ratio payer user_id user_ratio
channel
AdNonSense 11.34% 440 3880 2.59%
FaceBoom 12.20% 3557 29144 19.43%
LeapBob 3.06% 262 8553 5.70%
MediaTornado 3.57% 156 4364 2.91%
OppleCreativeMedia 2.71% 233 8605 5.74%
RocketSuperAds 7.91% 352 4448 2.97%
TipTop 9.60% 1878 19561 13.04%
WahooNetBanner 5.30% 453 8553 5.70%
YRabbit 3.83% 165 4312 2.87%
lambdaMediaAds 10.47% 225 2149 1.43%
organic 2.06% 1160 56439 37.62%
In [32]:
channel_count = channel_data['user_id'].sort_values().plot.barh(title='Рейтинг каналов по количеству пользователей')
channel_count.set(xlabel='Количество пользователей', ylabel='Канал');
No description has been provided for this image
In [33]:
channel_payer = channel_data['payer'].sort_values().plot.barh(title='Рейтинг каналов по количеству платящих пользователей')
channel_payer.set(xlabel='Количество платящих пользователей', ylabel='Канал');
No description has been provided for this image
In [34]:
channel_payer_ratio = (channel_data['payer_ratio']*100).sort_values().plot.barh(title='Рейтинг каналов по доле платящих пользователей')
channel_payer_ratio.set(xlabel='Доля платящих пользователей, %', ylabel='Канал');
No description has been provided for this image

По количеству привлеченных пользователей лидирует органический трафик (37,62% всех пользователей), что встречается довольно часто. Среди платных рекламных каналов лидирует Faceboom (19,43%), также среди остальных выделяется TipTop (13,04%).
По количеству платящих пользователей абсолютное лидерство у Faceboom, далее следуют TipTop и органический трафик.
По доле платящих пользователей лидирует Faceboom (12,2%), также в тройку лидеров входят AdNonSense (11,34%) и lambdaMediaAds (10,47%). Органический трафик на последнем месте (2,06%), однако это обусловлено наибольшим общим количеством пришедших пользователей (как уже было сказано, по количеству платящих пользователей органический трафик на 3-ем месте).

Основной страной присутствия для компании является США (2/3 всех пользователей и 77,72% платящих). Доля платяших в ней также наибольшая (6,9%).
Основным устройством, с которого пользователи используют приложение, является iPhone (36,32% всех пользователей и 38,08% платящих). По доле платящих лидером является Mac (6,36%), iPhone на 2-ом месте (6,21%). Больше всего пользователей в целом из органического трафика (37,62%), а платящих больше всего пришло через платформу Faceboom (примерно 3 500 человек), доля платящих самая высокая также у Faceboom (12,2%).

Маркетинг¶

Общая сумма расходов на маркетинг¶

In [35]:
costs['costs'].sum().round(2)
Out[35]:
105497.3

На рекламные расходы было потрачено примерно 105 497 у.е.

Распределение рекламных расходов по источникам¶

In [36]:
costs_grouped = costs.groupby('channel').agg({'costs':'sum'}).sort_values(by='costs', ascending=False)
costs_grouped['ratio'] = (costs_grouped['costs']/costs_grouped['costs'].sum()*100).round(2)
costs_grouped
Out[36]:
costs ratio
channel
TipTop 54751.30 51.90
FaceBoom 32445.60 30.75
WahooNetBanner 5151.00 4.88
AdNonSense 3911.25 3.71
OppleCreativeMedia 2151.25 2.04
RocketSuperAds 1833.00 1.74
LeapBob 1797.60 1.70
lambdaMediaAds 1557.60 1.48
MediaTornado 954.48 0.90
YRabbit 944.22 0.90
In [37]:
costs_plot = costs_grouped['costs'].sort_values().plot.barh(title='Рейтинг рекламных источников по затратам')
costs_plot.set(xlabel='Затраты', ylabel='Источник');
No description has been provided for this image

По рекламным затратам абсолютным лидером является TipTop (51,9%), далее следует FaceBoom (30,75%), все остальные источники составляют менее 5% взятый каждый в отдельности.

Динамика изменения рекламных расходов по источникам¶

In [38]:
costs['week'] = costs['dt'].astype('datetime64[W]')
costs['month'] = costs['dt'].astype('datetime64[M]')

Так как данные представлены за один 2019 год, то по для анализа динамики данные можно не группировать по году.

In [39]:
costs_week = costs.pivot_table(index='week', 
                  columns='channel',
                  values='costs', 
                  aggfunc='sum')
costs_week.loc['total', :] = costs_week.sum()
costs_week.sort_values(by='total', axis=1, inplace=True, ascending=False)
costs_week.drop(index='total', inplace=True)
In [40]:
costs_month = costs.pivot_table(index='month', 
                  columns='channel',
                  values='costs', 
                  aggfunc='sum')
costs_month.loc['total', :] = costs_month.sum()
costs_month.sort_values(by='total', axis=1, inplace=True, ascending=False)
costs_month.drop(index='total', inplace=True)
In [41]:
costs_week.plot.area(grid=True, figsize=(15,6), ax=plt.subplot(1,2,1), 
                                      xlabel='Номер месяца', ylabel='Сумма затрат', title='Понедельная динамика затрат по каналам')
costs_month.plot.area(grid=True, figsize=(15,6), ax=plt.subplot(1,2,2), 
                                      xlabel='Номер месяца', ylabel='Сумма затрат', title='Помесячная динамика затрат по каналам')

plt.tight_layout()
plt.show()
No description has been provided for this image

На обоих графиках в начале выше всех находится график Faceboom, а потом TipTop, далее лидерство перехватывает Tip Top и со временем отрыв TipTop от Faceboom растет.
Понедельный график более изрезанный, а помесячный более плавный, что подтверждает обратную взаимосвязь между шумами на графике и длиной рассматриваемого периода.
На помесячном графике заметен уверенный и стабильный рост источника TipTop с падением в последнем месяце.
Максимальная сумма недельных рекламных затрат составила примерно 6 800 у.е., месячных - примерно 22 000 у.е.

Средняя стоимость привлечения по источнику¶

In [42]:
cac_source = profiles.groupby('channel').agg({'acquisition_cost':'mean'}).sort_values(by='acquisition_cost', ascending=False)
cac_source['acquisition_cost']=cac_source['acquisition_cost'].round(2)
cac_source
Out[42]:
acquisition_cost
channel
TipTop 2.80
FaceBoom 1.11
AdNonSense 1.01
lambdaMediaAds 0.72
WahooNetBanner 0.60
RocketSuperAds 0.41
OppleCreativeMedia 0.25
YRabbit 0.22
MediaTornado 0.22
LeapBob 0.21
organic 0.00

Самая большая средняя стоимость привлечения пользователя у источника TipTop (2,8 у.е.), далее следует Faceboom (1,11 у.е.) и AdNonSense (1,01 у.е.). TipTop сильно выше остальных, что было заметно и в остальных показателях по рекламным затратам.

Всего на рекламу в рассматриваемый период было потрачено 105 497 у.е. Максимальная сумма затрат за неделю - 6 800 у.е., а за месяц - примерно 22 000 у.е. Самым дорогим с точки зрения рекламных затрат источником оказался TipTop (51,9% всех затрат), далее следует Faceboom (30,75% всех затрат). При этом стоит отметить, что у Faceboom средняя стоимость привлечения составляет 1,11 у.е., что значительно ниже, чем у TipTop (2,8 у.е.).
А количество пользователей, как совокупное, так и платящих, выше у Faceboom. Это позволяет сделать предположение о том, что Faceboom окупается лучше, чем TipTop.

Окупаемость рекламы¶

Органических пользователей в анализ окупаемости включать не нужно, так как рекламные затраты на их привлечение равны нулю.

In [43]:
observation_date = datetime(2019,11,1).date()
horizon = 14

Окупаемость рекламы в целом¶

In [44]:
ltv_raw, ltv, ltv_history, roi, roi_history = get_ltv(profiles, orders, observation_date, horizon)

Проверяем корректность расчета CAC:

In [45]:
max_acquisition_date = roi_history.reset_index()['dt'].max()
print(max_acquisition_date) 
2019-10-19
In [46]:
print(
   'Общие расходы на привлечение из таблицы ROI: {:.2f}'.format(
       roi.loc['All users', 'cac'] * roi.loc['All users', 'cohort_size']
   )
)

print(
   'Общие расходы на привлечение из таблицы costs: {:.2f}'.format(
       costs.query('dt <= @max_acquisition_date')['costs'].sum()
   )
) 
Общие расходы на привлечение из таблицы ROI: 99085.74
Общие расходы на привлечение из таблицы costs: 99085.74

CAC посчитан верно.

Построим графики LTV, CAC, ROI:

In [47]:
plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon)
No description has been provided for this image

Как видно на графике ROI, реклама, направленная на привлечение пользователей, не окупается в течение заложенного в бизнес-плане срока, равного 2 неделям.
Причина провала рекламной кампании кроется в увеличении рекламных расходов на привлечение пользователя в целом, что видно на графике динамики CAC, который возрастает, начиная с резкого роста в июне. Такой постоянный рост стал причиной постепенного падения ROI на 14 лайфтайм вплоть до уровня ниже окупаемости, начиная с середины июня.

Конверсия пользователей¶

In [48]:
conversion_raw, conversion, conversion_history = get_conversion(profiles, orders, observation_date, horizon)

Проверим правильность расчета конверсии:

In [49]:
print(conversion[conversion > 1].fillna(''))
           cohort_size 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 11.0 12.0  \
cohort                                                                          
All users        88644                                                          

          13.0  
cohort          
All users       

В таблице конверсии остался только размер когорты — CR выше единицы нет.

In [50]:
print(conversion_history[conversion_history > 1].fillna('')) 
            cohort_size 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 11.0  \
dt                                                                          
2019-05-01          390                                                     
2019-05-02          323                                                     
2019-05-03          346                                                     
2019-05-04          457                                                     
2019-05-05          438                                                     
...                 ...  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ...  ...   
2019-10-15          442                                                     
2019-10-16          436                                                     
2019-10-17          449                                                     
2019-10-18          591                                                     
2019-10-19          588                                                     

           12.0 13.0  
dt                    
2019-05-01            
2019-05-02            
2019-05-03            
2019-05-04            
2019-05-05            
...         ...  ...  
2019-10-15            
2019-10-16            
2019-10-17            
2019-10-18            
2019-10-19            

[172 rows x 15 columns]

В таблице динамики конверсии на месте значений по лайфтаймам одни пропуски. Значит, ни в одной из ячеек нет ошибочного значения конверсии — выше единицы.

Построим графики:

In [51]:
plot_conversion(conversion, conversion_history, horizon)
No description has been provided for this image

Конверсия плавно растет с последовательным замедлением роста, на 14 лайфтайм конверсия составила примерно 8%.
График динамики конверсии на 14-й день довольно сильно "шумит", диапазон значений примерно от 6,75% до 9,3%.

Удержание пользователей¶

In [52]:
retention_raw, retention, retention_history = get_retention(profiles, visits, observation_date, horizon)

Построим графики:

In [53]:
plot_retention(retention, retention_history, horizon)
No description has been provided for this image

Удержание платящих пользователей на следующий день после привлечения составляет примерно 38%, а через 2 недели - примерно 10%.
Удержание неплатящих пользователей на следующий день после привлечения составляет примерно 17,5%, а через 2 недели - близко к нулю.
На графике динамики удержания видно, что в течение рассматриваемого периода удержание платящих пользователей через 2 недели колебалось от 17,5% до 6,25%.
Удержание неплатящих через 2 недели близко к нулю на протяжении всего рассматриваемого периода.

Окупаемость рекламы с разбивкой по устройствам¶

In [54]:
ltv_raw, ltv, ltv_history, roi, roi_history = get_ltv(profiles, orders, observation_date, horizon, dimensions=['device'])
In [55]:
plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon)
No description has been provided for this image

iPhone, Mac, Android не окупаются в течение 2 недель при том, что LTV у них выше, чем у PC, который единственный окупился. Соответственно дело в более высокой стоимости привлечения пользователей iPhone, Mac, Android.
Таким образом, среди устройств негативное влияение на окупаемость оказывают iPhone, Mac, Android.

In [56]:
retention_raw, retention, retention_history = get_retention(profiles, visits, observation_date, horizon, dimensions=['device'])
In [57]:
plot_retention(retention, retention_history, horizon)
No description has been provided for this image

Удержание платящих пользователей на Mac, iPhone, Android ниже, чем на PC. Возможно, интерфейс приложения на данных устройствах является менее удобным и понятным, чем на PC. Однако разница в удержании не настолько велика, чтобы ее можно было рассматривать в качестве основной причины разницы в окупаемости.

In [58]:
conversion_raw, conversion, conversion_history = get_conversion(profiles, orders, observation_date, horizon, dimensions=['device'])
In [59]:
plot_conversion(conversion, conversion_history, horizon)
No description has been provided for this image

Пользователи Mac, iPhone, Android конвертируются сильно лучше PC.

Основной причиной неокупаемости iPhone, Mac, Android скорее всего является большее по сравнению с другими устройствами использование более дорогих рекламных платформ, которые стоят дороже (прежде всего, речь идет о TipTop и Faceboom).
Также можно предположить, что маркетологи компании при разработке рекламной кампании ориентируются на коэффициент конверсии, которые у iPhone, Mac, Android лучше, чем у остальных устройств. Заметив, что конверсия по данным устройствам лучше, маркетологи принимают решение вложить в рекламу на данных устройствах большие объемы средств, пытаясь привлечь все больше пользователей и игнорируя при этом, что увеличение рекламных расходов не приводит к желаемому увеличению конверсии и LTV (они растут умеренно теми же темпами, что и другие устройства), в результате чего и получаются негативные показатели окупаемости.

Окупаемость рекламы с разбивкой по странам¶

In [60]:
ltv_raw, ltv, ltv_history, roi, roi_history = get_ltv(profiles, orders, observation_date, horizon, dimensions=['region'])
In [61]:
plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon)
No description has been provided for this image

Реклама в США не окупается, в остальных странах реклама окупается с большим запасом (ROI на 14-й день составляет 140%-170%) при том, что LTV в США заметно выше, чем в остальных странах. Но еще более заметно, что в США выше стоимость привлечения пользователей (в 3 и даже почти в 4 раза), что видно на графике динамики CAC. Именно это и является причиной неокупаемости рекламы в США.

In [62]:
retention_raw, retention, retention_history = get_retention(profiles, visits, observation_date, horizon, dimensions=['region'])
In [63]:
plot_retention(retention, retention_history, horizon)
No description has been provided for this image

Удержание платящих пользователей в США заметно ниже, чем в остальных странах. Причиной этого может быть неправильный подход к населению страны и отсутствие учета национальных интересов жителей.

In [64]:
conversion_raw, conversion, conversion_history = get_conversion(profiles, orders, observation_date, horizon, dimensions=['region'])
In [65]:
plot_conversion(conversion, conversion_history, horizon)
No description has been provided for this image

Конверсия пользователей в США заметно выше, чем в остальных странах. Конверсия пользователей на 14-й день заметно выросла в июне, когда заметно выросли расходы на рекламу.

Таким образом, США оказывают сильное негативное влияние на окупаемость рекламной кампании всего приложения в целом. Заметно резкое увеличение расходов на рекламу в США при последующем увеличении конверсии пользователей.
Однако клиент должен приносить компании прибыль, а не просто совершать первую покупку, а в США этого не происходит, так как удержание платящих пользователей в США заметно ниже, чем в остальных странах.
Ориентация на показатель конверсии, которую, судя по всему, используют маркетологи компании, не оправдала себя.
Также возможной причиной неудачи рекламной кампании в США является большая по сравнению с другими странами распространенность более дорогих рекламных платформ, которые стоят дороже (прежде всего, речь идет о TipTop и Faceboom). Если обратиться к географическому распределению пользователей, то на пользователей США приходится 2/3 от их общего количества, а на платящих пользователей даже больше. С учетом высокого уровня окупаемости в других странах, стоит снизить рекламные расходы в США в пользу других стран, или попытаться выйти на новые рынки, если есть такая возможность.

Окупаемость рекламы с разбивкой по рекламным каналам¶

In [66]:
ltv_raw, ltv, ltv_history, roi, roi_history = get_ltv(profiles, orders, observation_date, horizon, dimensions=['channel'])
In [67]:
plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon)
No description has been provided for this image

На графике ROI видно, что не окупились 3 канала: AdNonSense, Faceboom, TipTop (хуже всех). При этом LTV лучше всех у lambdaMediaAds, а на 2-ом месте TipTop. По рекламным расходам абсолютным лидером является TipTop (в начале небольшое преимущество было у Faceboom), на 2-ом месте Faceboom, AdNonSense на 3-ем.
Таким образом, можно сказать о негативном влиянии на окупаемость каналов TipTop, Faceboom, AdNonSense. Это связано прежде всего с CAC, но не только: Faceboom и AdNonSense примерно в середине рейтинга LTV, что явно не является лучшими показателями.

In [68]:
retention_raw, retention, retention_history = get_retention(profiles, visits, observation_date, horizon, dimensions=['channel'])
In [69]:
plot_retention(retention, retention_history, horizon)
No description has been provided for this image

Аутсайдерами по удержанию платящих пользователей являются каналы Faceboom и AdNonSense. Это однозначно является одной из причин провальной окупаемости данных платформ и соответственно говорит об их негативном влиянии на окупаемость рекламы приложения в целом.

In [70]:
conversion_raw, conversion, conversion_history = get_conversion(profiles, orders, observation_date, horizon, dimensions=['channel'])
In [71]:
plot_conversion(conversion, conversion_history, horizon)
No description has been provided for this image

Лидером по конверсии является Faceboom, далее следуют AdNonSense, lambdaMediaAds, TipTop. То есть вновь повторяется та же история, что и в случае со странами и устройствами: провальные с точки зрения окупаемости и удержания каналы являются лидерами по конверсии.

Таким образом, основной причиной провальной окупаемости нескольких каналов являются высокие расходы на финансирование рекламы через них, а также крайне низкие показатели удержания клиентов.
Причиной столь серьезного финансирования могут быть высокие показатели конверсии у данных каналов, то есть маркетологи могут пытаться развивать каналы с более высокими конверсиями.
Причиной низких показателей удержания платящих пользователей могут быть особенности работы рекламных платформ. Платформы Faceboom, TipTop, AdNonSense оказывают негативное влияение на окупаемость рекламы приложения в целом.
Маркетологам, возможно, стоит переключиться на другие платформы, у которых хорошие показатели окупаемости. Например, хорошим кандидатом является lambdaMediaAds, у которой высокие показатели конверсии, удержания и окупаемости, а по LTV она вообще на 1-ом месте.

Выводы¶

В процессе предобработки данных были обнаружены следующие проблемы:

  • Некорректные типы данных в столбцах с датой и временем, которые пришлось заменить;
  • Названия всех столбцов были приведены к snake_case;
  • Какие-либо пропуски или дубликаты в данных обнаружены не были, что говорит о высоком качестве данных.

Причины неэффективности привлечения пользователей:

  • Неокупаемость рекламных расходов (ROI в целом менее 100%);
  • Неправильное использование рекламного бюджета: потенциальные точки роста остаются недофинансированными при избытке финансирования в сектора, которые уже сложно или невозможно развивать. Например, для канала TipTop CAC составляет 3,5 у.е. в конце периода, тогда как в других перспективных каналах этот показатель колеблется от 0 до 1 у.е.;
  • При планировании рекламного бюджета во внимание принимается прежде всего высокая конверсия в покупателей, при этом игнорируется тот факт, что причины более высоких уровней конверсии совсем необязательно связаны с более высоким финансированием (рост конверсии во всех секторах примерно одинаковый при огромной разнице в финансировании между некоторыми секторами). Например, самой высокой конверсией на 14-й день среди рекламных платформ обладают Faceboom и AdNonSense (12% и 11% соответственно), тогда как показатель CAC на конец периода у них на уровне чуть более 1 у.е., что является самым высоким показателем за исключением TipTop. Более того у них крайне низкие показатели удержания (менее 5% на 14-й день), тогда как у всех других платформ этот показатель выше 10%. Причинами низкого удержания на платформах могут быть особенности работы платформы;
  • При планировании рекламного бюджета в некоторых случаях игнорируются стабильно низкие показатели удержания, т.е. деньги направляются в такие сектора, хотя смысла это не имеет, если они не окупаются. Например, в США показатель удержания на 14-й день составляет менее 10%, тогда как в Великобритании и Франции он равен примерно 18%. При этом CAC для США в конце периода равен 1,7 у.е., а для Великобритании и Франции примерно 0,4 у.е.;
  • Топ-3 канала привлечения пользователей по количеству привлекаемых рекламных средств и количеству пользователей в целом одновременно являются 3 каналами, которые не окупаются: TipTop, Faceboom, AdNonSense (остальные каналы окупаются). ROI AdNonSense и Faceboom составляет 80-90%, а у TipTop вообще не более 60%.
  • Негативное влияние оказывает также географическое распределение пользователей: в стране основного присутствия США (2/3 всех пользователей и примерно 77% платящих приходится именно на одну эту страну) окупаемость по ROI около 70%, в то время как в других странах она составляет 140% и даже 170%. Обусловлено это высоким уровнем рекламных затрат в США, который, в свою очередь, обусловлен тем, что это основной для компании рынок. Иной причиной плохих показателей окупаемости в США является преимущественное использование рекламных платформ с низкой окупаемостью в этой стране. Также важно понимать, что в США самые низкие показатели удержания, что нельзя не учитывать при планировании рекламного бюджета;
  • Реклама на Mac, iPhone, Android также не окупается (ROI 70% у Mac, iPhone, а у Android 90% по сравнению с PC, у которого ROI примерно 105%), в то время как CAC на конец периода у них опять же самый высокий (примерно 1,5 у.е. у Mac, iPhone и 1,2 у.е. у Android по сравнению с 0,9 у.е. для PC).
  • Неэффективное распределение бюджетных средств по описанным выше принципам осуществлялось не месяц, и не два: оно продолжается на протяжении всего рассматриваемого периода и нет оснований предпологать, что оно прекратится в будущем. Если ситуацию не исправить, это рано или поздно приведет к закрытию бизнеса. Например, CAC платформы TipTop за рассматриваемый период вырос с 1 у.е. до 3,5 у.е. в конце периода.

Рекомендации для отдела маркетинга:

  • При плаинровании рекламного бюджета необходимо принимать во внимание не только конверсию, но и удержание, а также ROI как основной индикатор эффективности рекламной кампании;
  • Постоянный и непрерывный анализ эффективности осуществляемых рекламных кампаний, а также поиск новых точек роста вместо следования одним и тем же принципам без их пересмотра;
  • Необходимо постепенно сокращать финансиноварие рекламных платформ Faceboom, AdNonSense и TipTop и направить освободившиеся средства в другие потенциально прибыльные платформы. Можно немного увеличить финансирование lambdaMediaAds, которая показывает себя очень хорошо по всем показателям (конверсия на 14-й день чуть более 10%, удержание - почти 20%, ROI почти 250%), которые возможно удастся улучшить еще сильнее. Также не стоит забывать и о новых точках роста, например YRabbit, у которой самый высокий показатель ROI (260%) при низком CAC (примерно 0,25 у.е.). Дальнейшее финансирование этой платформы позволит узнать, увеличится ли конверсия и удержание пользователей на этой платформе и соответственно по приложению в целом или нет;
  • Рекомендуется обратить внимание на другие страны, кроме США. Судя по географическому распределению пользователей, США является основным рынком для приложения, однако это не говорит об эффективности дальнейшего финансирования рекламы в этой стране. Наоборот, возможно количество привлеченных пользователей уже достигло своего максимума на данном рынке, поэтому есть смысл постепенно снижать рекламные расходы в пользу любых других стран, как уже имеющихся (Франция, Германия, Великобритания), которые все показывают очень неплохие показатели удержания и окупаемости (ROI от 150 до 170%, удержание от 14% до 18%), так и других, ведь земной шар не ограничен четырьмя странами. Но в последнем случае предварительно стоит провести серьезное исследование потребительского поведения и предпочтений, возможных конкурентов, национальных особенностей ведения бизнеса и обычаев.